# This file is part of Neuroinfo Toolkit.
#
# Neuroinfo Toolkit is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Neuroinfo Toolkit is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Neuroinfo Toolkit.  If not, see <http://www.gnu.org/licenses/>.

import urllib
import httplib2
import neuro.arrays as arrays
import neuro.strings as strings
import neuro.filesystem as filesystem
from neuro.exceptions import IllegalArgumentException
from neuro.exceptions import FileNotFoundException
from neuro.net.transport import TransportException
from neuro.exceptions import BaseException
from urlparse import urlparse
from cgi import parse_qs

class HttpRequest(object):
    '''
    .. note:: For more advanced functionality, look at :py:mod:`urllib2`
    '''

    def __init__(self, url):
        '''
        Constructor ::
        
            >>> from neuro.net.http import HttpRequest
            >>> req = HttpRequest("https://www.google.com:80")
        
        :param url: URL
        :type url: str
        '''
        self._protocol = "http"
        self._method = None
        self._hostname = None
        self._port = 80
        self._path = "/"
        self._cookie = None
        self._referrer = None

        self._headers = {}
        self._postParams = {}
        self._queryParams = {}
        self._putData = None
        self._files = {}

        self._baseURL = None
        self._request = None
        self._lastURL = None

        self._username = None
        self._password = None
        self._response = None

        self.setURL(url)

    def setCredentials(self, username, password):
        '''
        Set Basic authentication credentials ::
            
            >>> req.setCredentials("jdoe", "p@$$w0rd") 

        :param username: Username
        :type username: str
        :param password: Password
        :type password: str
        '''
        if(not isinstance(username, basestring)):
            raise IllegalArgumentException("Username must be an instance of str")
        elif(not isinstance(password, basestring)):
            raise IllegalArgumentException("Password must be an instance of str")

        self._username = username
        self._password = password

    def setURL(self, url):
        '''
        Explode a URL for use by this class ::

            >>> req.setURL("http://www.google.com:80")

        :param url: URL
        :type url: str
        :raises: :class:`URLFormatException`
        '''
        if(not isinstance(url, basestring)):
            raise IllegalArgumentException("URL must be an instance of str")

        url = url.strip()
        
        if(url == ""):
            raise IllegalArgumentException("URL cannot be empty")

        parsed = urlparse(url)
        
        if(parsed.scheme == ""):
            raise URLFormatException(url)
        elif(parsed.hostname == ""):
            raise URLFormatException(url)
        
        self._protocol = parsed.scheme
        self._hostname = parsed.hostname

        if(parsed.port == None):
            if(self._protocol == "http"):
                self._port = 80
            elif(self._protocol == "https"):
                self._port = 443
        else:
            self._port = parsed.port

        if(parsed.path == ""):
            self._path = "/"
        else:
            self._path = parsed.path

        if(parsed.query != ""):
            params = parse_qs(parsed.query)
            
            for key in params.keys():
                params[key] = arrays.flatten(params[key])
            
            self._queryParams = dict(self._queryParams.items() + params.items())

        if(parsed.username != None):
            self._username = parsed.username

        if(parsed.password != None):
            self._password = parsed.password

    def addHeader(self, key, value):
        '''
        Set a request header ::
        
            >>> req.addHeader("Content-Type", "text/html")
            
        :param key: Header key
        :type key: str
        :param value: Header value
        :type value: str
        '''   
        if(not isinstance(key, basestring)):
            raise IllegalArgumentException("HTTP request header key must be an instance of str")
        elif(not isinstance(value, basestring)):
            raise IllegalArgumentException("HTTP request header value must be an instance of str")

        key = key.strip()
        value = value.strip()

        if(key == ""):
            raise IllegalArgumentException("HTTP request header key cannot be empty")

        self._headers[key] = value

    def setHostname(self, hostname):
        '''
        Set the hostname ::
        
            >>> req.setHostname("www.google.com")
            
        :param hostname: Hostname
        :type hostname: str
        '''
        if(not isinstance(hostname, basestring)):
            raise IllegalArgumentException("HTTP request hostname must be an instance of str")

        hostname = hostname.strip()

        if(hostname == ""):
            raise IllegalArgumentException("HTTP request hostname cannot be empty")

        self._hostname = hostname

    def setPort(self, port=80):
        '''
        Set the port ::
        
            >>> req.setPort(80)
            
        :param port: Port
        :type port: int
        '''
        if(not isinstance(port, int)):
            raise IllegalArgumentException("HTTP request port must be an instance of int")

        if(port < 0 or port > 65535):
            raise IllegalArgumentException("Port must be in the range 0 => 65535")
        
        self._port = port

    def setMethod(self, method="GET"):
        '''
        Set the request method/verb e.g. "GET", "POST", "PUT", "DELETE" ::
        
            >>> req.setMethod("get")
            
        :param method: Request method  
        :type method: str
        '''
        if(not isinstance(method, basestring)):
            raise IllegalArgumentException("HTTP method/verb must be an instance of str")

        method = method.strip().upper()

        if(method != "GET" and method != "POST" and method != "PUT" and method != "DELETE"):
            raise IllegalArgumentException("HTTP request method/verb must be one of: \"GET\", \"POST\", \"PUT\", or \"DELETE\"")

        self._method = method

    def setProtocol(self, protocol="http"):
        '''
        Set the HTTP protocol/scheme ::

            >>> req.setProtocol("https")
            
        :param protocol: "http" or "https"
        :type protocol: str
        '''
        if(not isinstance(protocol, basestring)):
            raise IllegalArgumentException("HTTP protocol/scheme must be an instance of str")

        protocol = protocol.strip()
        
        if(protocol == ""):
            raise IllegalArgumentException("HTTP protocol/scheme cannot be empty")

        ## --- remove any "://"
        match = strings.regex("^(https?)", protocol)

        if(match):
            self._protocol = match[0]
        else:
            raise IllegalArgumentException("HTTP protocol/scheme must be one of: http or https")

    def addPostParam(self, key, value=""):
        '''
        Set a POST parameter ::
        
            >>> req.addPostParam("foo", "bar")
            
        :param key: POST parameter key
        :type key: str
        :param value: POST parameter value
        :type value: str
        '''
        if(not isinstance(key, basestring)):
            raise IllegalArgumentException("POST parameter key must be an instance of str")
        elif(not isinstance(value, basestring)):
            raise IllegalArgumentException("POST parameter value must be an instance of str")

        key = key.strip()
        value = value.strip()

        if(key == ""):
            raise IllegalArgumentException("POST parameter key cannot be empty")

        self._postParams[key] = value

    def addFileParam(self, key, filename):
        '''
        Set a file parameter ::
        
            >>> req.addFileParam("file", "/path/to/file.txt")
            
        :param key: File parameter key
        :type key: str 
        :param filename: File name
        :type filename: str
        '''
        if(not isinstance(key, basestring)):
            raise IllegalArgumentException("POST file key must be an instance of str")
        elif(not isinstance(filename, basestring)):
            raise IllegalArgumentException("POST file name must be an instance of str")

        key = key.strip()
        filename = filesystem.canonical(filename)

        if(key == ""):
            raise IllegalArgumentException("POST file key cannot be empty")
        elif(filename == ""):
            raise IllegalArgumentException("POST file name cannot be empty")

        if(not exists(filename)):
            raise FileNotFoundException(filename)

        self._files[key] = vilename

    def addQueryParam(self, key, value=""):
        '''
        Set a query string parameter
        
            >>> req.addQueryParam("foo", "bar")
            
        :param key: Query parameter key
        :type key: str
        :param value: Query parameter value
        :type value: str
        '''
        if(not isinstance(key, basestring)):
            raise IllegalArgumentException("Query key must be an instance of str")
        elif(not isinstance(value, basestring)):
            raise IllegalArgumentException("Query value must be an instance of str")

        key = key.strip()
        value = value.strip()

        if(key == ""):
            raise IllegalArgumentException("Query key cannot be empty")

        self._queryParams[key] = value

    def setPath(self, path="/"):
        '''
        Set the URL path e.g. /foo/bar ::
        
            >>> req.setPath("/foo/bar")
            
        :param path: URL path
        :type path: str
        '''
        if(not isinstance(path, basestring)):
            raise IllegalArgumentException("URL path must be an instance of str")

        path = path.strip()

        if(path == ""):
            raise IllegalArgumentException("URL path cannot be empty")
        
        if(path[0] != "/"):
            path = "/" + path

        self._path = path

    def send(self, socktimeout=30):
        '''
        Send HTTP request ::
        
            >>> req.send()
            
        :param socktimeout: Socket timeout
        :type socktimeout: int
        :returns: HTTP response object
        :rtype: :class:`HttpResponse`
        :raises: :class:`~neuro.net.transport.TransportException`
        '''
        if(not isinstance(socktimeout, int)):
            raise IllegalArgumentException("Timeout must be an instance of int")
        elif(socktimeout <= 0):
            raise IllegalArgumentException("Timeout cannot be <= 0")
        
        self.completePath()

        host = self._protocol + "://" + self._hostname + ":" + str(self._port)
        url = host + self._path

        headers = {}

        if(self._postParams and self._putData and self._method == None):
            raise Exception("Request has both POST and PUT data and no method set. Not sure what to do.")

        if(self._method is None):
            if(self._postParams):
                self._method = "POST"
                headers = {'Content-type': 'application/x-www-form-urlencoded'}
            elif(self._putData):
                self._method = "PUT"
            else:
                self._method = "GET"

        if(self._cookie != None):
            headers["Cookie"] = self._cookie

        # Previously: http = httplib2.Http(timeout=socktimeout)
        # Had to remove timeout, b/c I was getting an error
        # "TypeError: __init__() got an unexpected keyword argument 'timeout'"
        http = httplib2.Http()

        if(self._username != None):
            if(self._password == None):
                self._password = ""
                
            http.add_credentials(self._username, self._password)
        
        ## --- for compatibility with httplib2 <= 0.2
        if(hasattr(http, "timeout")):
            http.timeout = socktimeout

        self._lastURL = url

        body=None

        if (self._postParams!=None) and (len(self._postParams)>0):
            body=urllib.urlencode(self._postParams)
        elif (self._putData!=None) and (len(self._putData)>0):
            body=self._putData

        try:
            response, content = http.request(url, self._method, headers=headers, body=body)
        except httplib2.ServerNotFoundError, e:
            raise TransportException("Could not create connection to " + host+": "+str(e))
        except Exception, e:
            # See Bug: http://code.google.com/p/httplib2/issues/detail?id=62
            raise TransportException("Could not create connection to " + host+": "+str(e))
        
        if("set-cookie" in response):
            self._cookie = response["set-cookie"]
            
        self._response = HttpResponse()
        self._response.setBody(content)
        self._response.setStatus(int(response["status"]))

        self.clear()
        
        return self._response

    def getLastResponse(self):
        '''
        Get last HTTP response

        :rtype: :class:`HttpResponse`
        '''
        return self._response

    def getLastURL(self):
        '''
        Get last requested URL

        :rtype: str
        '''
        return self._lastURL

    def clear(self):
        '''
        Clear request parameters and URL path. Keep cookies.
        '''
        self._headers = {}
        self._postParams = {}
        self._queryParams = {}
        self._putData = None
        self._files = {}
        self._method = None
        self._path = "/"
        self._fullURL = None

    def getURL(self):
        '''
        Get the full URL

        :rtype: str
        '''
        return self._protocol + "://" + self._hostname + ":" + str(self._port) + self._path

    def setCookie(self, cookie):
        '''
        Set cookie for subsequent requests

            >>> http.setCookie("foo")
            
        :param cookie:
        :type cookie: str
        '''
        if(not isinstance(cookie, basestring)):
            raise IllegalArgumentException("Cookie must be an instance of str")
        elif(cookie == ""):
            raise IllegalArgumentException("Cookie cannot be null")

        self._cookie = cookie

    def getCookie(self):
        '''
        Get cookie

            >>> http.getCookie("foo")
            
        :rtype: str
        '''
        return self._cookie

    def getPostParams(self):
        '''
        Get all POST parameters

        :rtype: dict
        '''
        return self._postParams

    def getQueryParams(self):
        '''
        Get all query parameters
        
        :rtype: dict
        '''
        return self._queryParams

    def completePath(self):
        '''
        Add any query params to path
        
            >>> http.completePath()
        '''
        if(len(self._queryParams) > 0):
            self._path += "?"

            for key, value in self._queryParams.iteritems():
                self._path += str(key) + "=" + str(value) + "&"

            self._path.strip("&")

        return self._path

    def getRequestObject(self):
        '''
        Retrieve the actual HTTP connection object

            >>> request = http.getRequestObject()
            
        :rtype: :class:`HTTPConnection`, :class:`HTTPSConnection`
        '''
        return self._request

class HttpResponse(object):
    '''
    HTTP response model
    '''

    def __init__(self):
        '''
        Constructor
        
            >>> response = HttpResponse()
        '''
        self._headers = {}
        self._status = None
        self._body = None
    
    def addHeader(self, key, value=""):
        '''
        Set HTTP response header
        
            >>> response.setHeader("foo", "bar")
            
        :param key:
        :type key: str
        :param value:
        :type value: str
        '''
        if(not isinstance(key, basestring)):
            raise IllegalArgumentException("HTTP response header key must be an instance of str")
        elif(key == ""):
            raise IllegalArgumentException("HTTP reesponse header key cannot be null")

        if(not isinstance(value, basestring)):
            raise IllegalArgumentException("HTTP response header value must be an instance of str")

        self._headers[key] = value
    
    def setBody(self, body):
        '''
        Set response body
        
            >>> response.setBody("<html />")
            
        :param body:
        :type body: str
        '''
        if(not isinstance(body, basestring)):
            raise IllegalArgumentException("HTTP response body must be an instance of str")

        self._body = body
    
    def getHeader(self, key):
        '''
        Retrieve a header by key
        
            >>> value = response.getHeader("Date")
            
        :param key:
        :type key: str
        :rtype: str
        '''
        if(not isinstance(key, basestring)):
            raise IllegalArgumentException("HTTP response header key must be an instance of str")

        if(key == ""):
            raise IllegalArgumentException("HTTP response header key cannot be null")

        if(key not in self._headers):
            raise ResponseHeadersException(self, key)
        else:
            return self._headers[key]
    
    def getBody(self):
        '''
        Retrieve the response body
        
            >>> body = response.getBody()
            
        :rtype: str
        '''
        return self._body

    def setStatus(self, status=404):
        '''
        Retrieve the response status

            >>> response.setStatus(200)
            
        :param status:
        :type status: int
        '''
        if(not isinstance(status, int)):
            raise IllegalArgumentException("HTTP response status must be an instance of int")

        if(status < 0):
            raise IllegalArgumentException("HTTP response status cannot be negative")

        self._status = status

    def getStatus(self):
        '''
        Get HTTP response status

            >>> status = response.getStatus()
            
        :rtype: int
        '''
        return self._status

class URLFormatException(BaseException):
    '''
    URL format exception
    '''
    def __init__(self, url):
        '''
        Constructor

        :param url:
        :type url: str
        '''
        BaseException.__init__(self)

        if(not isinstance(url, basestring)):
            raise IllegalArgumentException("URL must be an instance of str")

        url = url.strip()

        if(url == ""):
            raise IllegalArgumentException("URL cannot be empty")

        self._url = url

    def getURL(self):
        '''
        Get the url that triggered this exception

        :rtype: str
        '''
        return self._url

    def getMessage(self):
        '''
        Get custom message

        :rtype: str
        '''
        return "Invalid URL format: " + self._url

class HttpResponseException(BaseException):
    '''
    HTTP response exception
    '''

    def __init__(self, response):
        '''
        Constructor

        :param response:
        :type response: :class:`HttpResponse`
        '''
        BaseException.__init__(self)

        if(not isinstance(response, HttpResponse)):
            raise IllegalArgumentException("Input parameter must be an instance of HttpResponse")

        self._response = response

    def getResponse(self):
        '''
        Get HttpResponse reference
        
        :rtype: :class:`HttpResponse`
        '''
        return self._response

class ResponseHeadersException(HttpResponseException):
    '''
    HTTP response headers exception
    '''
    MISSING_HDR=1

    def __init__(self, type, response, key):
        '''
        Constructor

        :param type: ResponseHeadersException.MISSING_HDR, etc.
		:type type: int
        :param response:
        :type response: :class:`HttpResponse`
        :param key:
        :type key: str
        '''
        HttpResponseException.__init__(self, response)
        
        if(not isinstance(type, int)):
            raise IllegalArgumentException("Exception type must be an instance of int")
        elif(not isinstance(key, basestring)):
            raise IllegalArgumentException("Response header key must be an instance of str")

        key = key.strip()

        if(type != 1):
            raise IllegalArgumentException("Exception type must be ResponseHeadersException.MISSING_HDR")
        elif(key == ""):
            raise IllegalArgumentException("Response header key cannot be empty")

        self._type = type
        self._response = response
        self._key = key

    def getType(self):
        '''
        Get exception type

        :rtype: int
        '''
        return self._type

    def getMessage(self):
        '''
        Get custom message

        :rtype: str
        '''
        if(self._type == ResponseHeadersException.MISSING_HDR):
            return "Missing HTTP response header \"" + self._key + "\""

class HttpRequestException(BaseException):
    '''
    HTTP Request Exception
    '''
    RESPONSE_STATUS=1

    def __init__(self, type, request):
        '''
        Constructor

        :param type:
        :type type: int
        :param request:
        :type request: :class:`HttpRequest`
        '''
        self._type = type
        self._request = request

    def getRequest(self):
        '''
        Get HttpRequest that caused this exception

        :rtype: :class:`HttpRequest`
        '''
        return self._request

    def getMessage(self):
        '''
        Get error message

        :rtype: str
        '''
        if(self._type == HttpRequestException.RESPONSE_STATUS):
            return "Received HTTP response code '" + str(self._request.getLastResponse().getStatus()) + "', URL=" + self._request.getLastURL()

    def __str__(self):
        return self.getMessage()
